using System;
using System.Text;
using System.Management.Automation;
using System.Management.Automation.Provider;
using System.Reflection;
using System.Collections.Generic;
using System.Xml;
using System.IO;


namespace Pscx.Commands
{
    #region NewPSDocCommand

    /// <summary>
    /// Generates a help XML document for PowerShell snap-ins based on the attributes and XML comments 
    /// set within the cmdlets.  It also requires the assembly have a corresponding XML document 
    /// built by the compiler. 
    /// </summary>
    [Cmdlet(VerbsCommon.New, "PsDoc", SupportsShouldProcess=true, DefaultParameterSetName="File")]
    public class NewPsDocCommand : PSCmdlet
    {
        const string COMMAND_URI = "http://schemas.microsoft.com/maml/dev/command/2004/10";
        const string MAML_URI = "http://schemas.microsoft.com/maml/2004/10";
        #region Private Data
        private XmlDocument helpDoc;
        private Assembly cmdletAssembly;
        private XmlDocument _XmlCompiledDoc;
        #endregion Private Data
 

        #region Parameters

        private string _file;
        /// <summary>
        /// File to generate PowerShell help files for.
        /// </summary>
        [Parameter(ValueFromPipeline=true, Mandatory=true, Position=0)]
        [Alias("File")]
        [ValidateNotNullOrEmpty]
        public string CmdletFile
        {
            get { return _file; }
            set { _file = value; }
        }
 
        private string _xmlDocFile;

        /// <summary>
        /// Corresponding XML documentation file. If left blank it will assume an .xml file in the same 
        /// folder with the same name as the assembly file. (ex. MyCmdlets.dll and MyCmdlets.xml)
        /// </summary>
        [Parameter(ValueFromPipeline=false, Mandatory=false, Position=1)]
        public string XmlDocFile
        {
            get
            {
                return _xmlDocFile;
            }
            set
            {
                _xmlDocFile = value;
            }
        }

        
        #endregion Parameters

        #region Cmdlet Overrides
        
        /// <summary>
        /// Begins New-PsDoc processing.
        /// </summary>
        protected override void BeginProcessing()
        {
            base.BeginProcessing();

            helpDoc = new XmlDocument();

            if (String.IsNullOrEmpty(_xmlDocFile))
                _xmlDocFile = _file.Replace(".dll", ".xml");

         }

        /// <summary>
        /// Processes the assembly and writes out the appropriate help.xml file for the cmdlet.
        /// </summary>
        protected override void ProcessRecord()
        {
            if (!ShouldProcess(_file))
                return;

            if (!File.Exists(_file))
            {
                WriteError(new ErrorRecord(new FileNotFoundException("Assembly file not found", _file), "1", ErrorCategory.InvalidData, null));
                return;
            }

            cmdletAssembly = Assembly.LoadFile(_file);
            _XmlCompiledDoc = new XmlDocument();
            XmlTextReader xtr = new XmlTextReader(File.OpenRead(_xmlDocFile));
            xtr.MoveToContent();

            _XmlCompiledDoc.AppendChild(_XmlCompiledDoc.ReadNode(xtr));

            XmlNode helpItems = helpDoc.CreateNode("element", "helpItems", "http://msh");
            XmlAttribute schemaAttr = helpDoc.CreateAttribute("schema");
            schemaAttr.Value = "maml";
            helpItems.Attributes.Append(schemaAttr);

            helpDoc.AppendChild(helpDoc.CreateXmlDeclaration("1.0", "utf-8", "yes"));
            helpDoc.AppendChild(helpItems);
            List<Type> _cmdlets = GetCmdletTypes(cmdletAssembly);
            foreach (Type cmdlet in _cmdlets)
            {
                helpItems.AppendChild(CreateCommandNode(cmdlet));
            }

            using (XmlTextWriter xtw = new XmlTextWriter(_file + "-Help.xml", Encoding.UTF8))
            {
                helpDoc.WriteContentTo(xtw);
            }
        }

        private XmlNode CreateCommandNode(Type cmdlet)
        {
            XmlNode cmdNode = helpDoc.CreateNode(XmlNodeType.Element, "command:command", COMMAND_URI);
            
            XmlAttribute nsAttribute = helpDoc.CreateAttribute("xmlns:command");
            nsAttribute.Value = COMMAND_URI;
            cmdNode.Attributes.Append(nsAttribute);

            nsAttribute = helpDoc.CreateAttribute("xmlns:dev");
            nsAttribute.Value = COMMAND_URI;
            cmdNode.Attributes.Append(nsAttribute);

            nsAttribute = helpDoc.CreateAttribute("xmlns:maml");
            nsAttribute.Value = MAML_URI;
            cmdNode.Attributes.Append(nsAttribute);

            cmdNode.AppendChild(CreateCommandDetailsNode(cmdlet));
            cmdNode.AppendChild(CreateDescriptionNode(cmdlet));
            cmdNode.AppendChild(CreateSyntaxNode(cmdlet));
            cmdNode.AppendChild(CreateParametersNode(cmdlet));

            return cmdNode;
        }
        
        private XmlNode CreateCommandDetailsNode(Type cmdlet)
        {
            XmlNode cmdDetailNode = helpDoc.CreateNode(XmlNodeType.Element, "command:details", COMMAND_URI);
            XmlNode cmdNameNode = helpDoc.CreateNode(XmlNodeType.Element, "command:name", COMMAND_URI);
            XmlNode cmdDescriptionNode = helpDoc.CreateNode(XmlNodeType.Element, "maml:description", MAML_URI);
            XmlNode cmdCopyrightNode = helpDoc.CreateNode(XmlNodeType.Element, "maml:copyright", MAML_URI);
            XmlNode cmdVerbNode = helpDoc.CreateNode(XmlNodeType.Element, "command:verb", COMMAND_URI);
            XmlNode cmdNounNode = helpDoc.CreateNode(XmlNodeType.Element, "command:noun", COMMAND_URI);

            CmdletAttribute attr = cmdlet.GetCustomAttributes(typeof(CmdletAttribute), false)[0] as CmdletAttribute;
            
            cmdNameNode.InnerText = String.Format("{0}-{1}", attr.VerbName, attr.NounName);
            cmdVerbNode.InnerText = attr.VerbName;
            cmdNounNode.InnerText = attr.NounName;

            AssemblyCopyrightAttribute attr2 = cmdlet.Assembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), true)[0] as AssemblyCopyrightAttribute;
            cmdCopyrightNode.AppendChild(GetParaNode(attr2.Copyright));

            cmdDescriptionNode.AppendChild(GetParaNode(GetCmdletDescriptionFromXmlDoc(cmdlet)));

            cmdDetailNode.AppendChild(cmdNameNode);
            cmdDetailNode.AppendChild(cmdCopyrightNode);
            cmdDetailNode.AppendChild(cmdDescriptionNode);
            cmdDetailNode.AppendChild(cmdVerbNode);
            cmdDetailNode.AppendChild(cmdNounNode);

            return cmdDetailNode;
        }

        private XmlNode CreateDescriptionNode(Type cmdlet)
        {
            return CreateDescriptionNode(GetCmdletDescriptionFromXmlDoc(cmdlet));
        }

        private XmlNode CreateDescriptionNode(string description)
        {
            XmlNode cmdDescriptionNode = helpDoc.CreateNode(XmlNodeType.Element, "maml:description", MAML_URI);
            cmdDescriptionNode.AppendChild(GetParaNode(description));
            return cmdDescriptionNode;
        }

        private XmlNode CreateSyntaxNode(Type cmdlet)
        {
            XmlNode syntaxNode = helpDoc.CreateNode(XmlNodeType.Element, "command:syntax", COMMAND_URI);
            XmlNode syntaxItemNode = helpDoc.CreateNode(XmlNodeType.Element, "command:syntaxItem", COMMAND_URI);
            XmlNode nameNode = helpDoc.CreateNode(XmlNodeType.Element, "maml:name", MAML_URI);

            CmdletAttribute attr = cmdlet.GetCustomAttributes(typeof(CmdletAttribute), false)[0] as CmdletAttribute;
            PropertyInfo[] paramInfoArray = cmdlet.GetProperties();

            nameNode.InnerText = String.Format("{0}-{1}", attr.VerbName, attr.NounName);
            
            syntaxNode.AppendChild(syntaxItemNode);
            syntaxItemNode.AppendChild(nameNode);

            foreach (PropertyInfo pi in paramInfoArray)
            {
                if (pi.GetCustomAttributes(typeof(ParameterAttribute), false).Length > 0)
                {
                    syntaxItemNode.AppendChild(CreateSingleParamNode(pi));
                }
            }
            return syntaxNode;
        }

        private XmlNode CreateParametersNode(Type cmdlet)
        {
            XmlNode paramsNode = helpDoc.CreateNode(XmlNodeType.Element, "command:parameters", COMMAND_URI);

            PropertyInfo[] paramInfoArray = cmdlet.GetProperties();
            foreach (PropertyInfo pi in paramInfoArray)
            {
                if (pi.GetCustomAttributes(typeof(ParameterAttribute), false).Length > 0)
                {
                    paramsNode.AppendChild(CreateSingleParamNode(pi));
                }
            }
            return paramsNode;
        }

        private XmlNode CreateSingleParamNode(PropertyInfo pi)
        {
            string paramName = GetParamName(pi);
            string paramDesc = GetDescriptionFromXmlDoc("P", String.Format("{0}.{1}", pi.DeclaringType, pi.Name));
            Type paramType;
            try
            {
                paramType = pi.PropertyType;
            }
            catch
            {
                paramType = typeof(System.Object);
            }

            ParameterAttribute paramAttr = pi.GetCustomAttributes(typeof(ParameterAttribute), false)[0] as ParameterAttribute;
            bool required = paramAttr.Mandatory;
            bool pipelineInput = paramAttr.ValueFromPipeline;
            int position = paramAttr.Position;

            return CreateSingleParamNode(paramName, paramDesc, paramType, required, pipelineInput, position, String.Empty);
        }

        private XmlNode CreateSingleParamNode(string name, string description, Type paramType, bool required, bool pipelineInput, int position, string defaultValue)
        {
            XmlNode paramNode = helpDoc.CreateNode(XmlNodeType.Element, "command:parameter", COMMAND_URI);
            XmlAttribute requiredAttr = helpDoc.CreateAttribute("required");
            XmlAttribute pipelineInputAttr = helpDoc.CreateAttribute("pipelineInput");
            XmlAttribute positionAttr = helpDoc.CreateAttribute("position");
            XmlElement paramNameNode = helpDoc.CreateElement("maml:name", MAML_URI);
            XmlNode paramDescNode;
            XmlElement paramValueNode = helpDoc.CreateElement("command:parameterValue");
            XmlElement devTypeNode = helpDoc.CreateElement("dev:type", COMMAND_URI);
            XmlElement devTypeNameNode = helpDoc.CreateElement("maml:name");
            XmlElement defaultValueNode = helpDoc.CreateElement("dev:defaultValue");

            requiredAttr.InnerText = (required ? "true" : "false");
            pipelineInputAttr.InnerText = pipelineInput ? "true" : "false";
            positionAttr.InnerText = position.ToString();
            paramNameNode.InnerText = name;
            paramDescNode = CreateDescriptionNode(description);
            paramValueNode.InnerText = paramType.ToString();
            devTypeNameNode.InnerText = paramType.Name;
            defaultValueNode.InnerText = defaultValue;

            paramNode.Attributes.Append(requiredAttr);
            paramNode.Attributes.Append(pipelineInputAttr);
            if(position != int.MinValue)
                paramNode.Attributes.Append(positionAttr);

            paramNode.AppendChild(paramNameNode);
            paramNode.AppendChild(paramDescNode);
            paramNode.AppendChild(paramValueNode);
            paramNode.AppendChild(devTypeNode);
                            
            devTypeNode.AppendChild(devTypeNameNode);

            return paramNode;
        }

        private string GetParamName(PropertyInfo pi)
        {
            AliasAttribute[] aliasAttr = pi.GetCustomAttributes(typeof(AliasAttribute), false) as AliasAttribute[];

            if (aliasAttr.Length == 0)
                return pi.Name;
            else
                return aliasAttr[0].AliasNames[0];
        }

        private XmlNode CreateInputTypesNode(Type cmdlet)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        private XmlNode CreateTerminatingErrorsNode(Type cmdlet)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        private XmlNode CreateExamplesNode(Type cmdlet)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        private XmlNode CreateRelatedLinksNode(Type cmdlet)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        #endregion Overrides

        private List<Type> GetCmdletTypes(Assembly cmdletAssembly)
        {
            List<Type> returnList = new List<Type>();
            Type[] types = cmdletAssembly.GetTypes();
            foreach (Type type in types)
            {
                if (type.GetCustomAttributes(typeof(CmdletAttribute), false).Length > 0 &&
                    type.IsSubclassOf(typeof(PSCmdlet)))
                {
                    returnList.Add(type);
                }
            }
            return returnList;
        }
        
        private XmlNode GetParaNode(string paraText)
        {
            XmlNode paraNode = helpDoc.CreateNode(XmlNodeType.Element, "maml:para", MAML_URI);
            paraNode.InnerText = paraText;
            return paraNode;
        }

        private string GetCmdletDescriptionFromXmlDoc(Type cmdlet)
        {
            return GetDescriptionFromXmlDoc("T", cmdlet.ToString());
        }

        private string GetDescriptionFromXmlDoc(string type, string name)
        {
            XmlNode descNode = _XmlCompiledDoc.SelectSingleNode(String.Format("descendant::member[@name='{0}:{1}']", type, name));
            
            if(descNode == null)
                return (type + ":" + name + " is missing an Xml Comment");
            string desc = descNode.InnerText.Trim();
            desc = desc.Replace("\n", String.Empty);
            desc = desc.Replace("\t", String.Empty);
            while (desc.Contains("  "))
                desc = desc.Replace("  ", " ");
            return desc;
        }
    } 

    #endregion
}
